<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>数独</title>
    <style>
        body {
            text-align: center
        }

        .list {
            position: absolute;
            top: 40%;
            left: 50%;
            transform: translate(-50%, -50%);
        }

        ul, li {
            margin: 0;
            padding: 0;
            list-style: none;
        }

        ul {
            border: 4px solid #000;
            border-right: none;
            width: 473px;
            counter-reset: section -1;
        }

        ul::after {
            content: '';
            display: table;
            clear: both;
        }

        li {
            float: left;
            border: 1px solid #5f666c;
            width: 50px;
            padding: 4px 0;
            position: relative;
        }

        li[data-tip]:hover::before {
            position: absolute;
            content: attr(data-tip);
            top: -36px;
            color: #ef4608;
            background: #fff;
            border: 2px solid #818181;
            border-radius: 3px;
            left: 50%;
            padding: 3px;
            transform: translate(-50%, 0%);
            z-index: 1;
        }

        li:after {
            position: absolute;
            left: 0;
            color: #f1f0f0;
            font-size: 24px;
            width: 50px;
            z-index: -1;
            counter-increment: section 1;
            content: counter(section);
        }

        li:nth-child(3n) {
            border-right: 2px solid #000;
        }
        li:nth-child(n+19) {
            border-bottom: 2px solid #000;
        }

        li:nth-child(n+28) {
            border-bottom: 1px solid #5f666c;
        }

        li:nth-child(n+46) {
            border-bottom: 2px solid #000;
        }

        li:nth-child(n+55) {
            border-bottom: 1px solid #5f666c;
        }
        li:nth-child(9n) {
            border-right: 4px solid #000;
        }
        input {
            border: none;
            background-color: transparent !important;
            padding: 0;
            width: 100%;
            text-align: center;
            font-size: 30px;
            color: #5d5d5d;
        }

        input:valid {
            background: #fff !important;
        }

        input[disabled] {
            color: #029a02;
        }

        input.lock {
            color: #104cbc;
        }

        input:focus {
            outline: none;
        }

        h2 {
            font-size: 28px;
        }

        button {
            margin-top: 20px;
            padding: 4px 20px;
            text-align: center;
            font-size: 20px;
        }

        .level {
            margin-top: 14px;
        }

        .level div {
            text-align: center;
        }
    </style>
</head>
<body>
<div class="list">
    <!--    <h2>数独</h2>-->
    <ul></ul>
    <button type="button" name="rand">随机</button>
    <button type="button" name="begin">解题</button>
    <button type="button" name="result" data-index="0">答案</button>
    <div class="level">
        <input type="range" min="0" value="0.3" step="0.1" max="0.7" title="随机难度">
        <div>中级</div>
    </div>
</div>
<script>
  'use strict'
  /*
  * 解题思路：
  *   1、过滤出所有空格，并计算出可能值._val，排序后提取可能值越少的元素放入尝试数组testList。
  *   2、从testList中取出一个元素，尝试可能值：
  *     2.1、testList中已经无值，说明尝试结束。
  *     2.1、尝试失败，删除此元素，返回上一步。
  *     2.3、尝试成功，继续尝试当前元素其他可能值。
  *     2.4、尝试未完成，继续第1步，并加入尝试数组继续尝试。
  *
  * 功能：
  *     一、出题：
  *       1、手动出题（输入过程自动纠正）
  *       2、随机出题（一键生成）
  *         2.1、难度调整
  *     二、解题：
  *          1、手动解题
  *             1.1、是否开启自动纠正
  *             1.2、是否开启提示
  *             1.3、确认（校验结果）
  *          2、自动解题
  *             2.1、是否开启秒解（否则需要按下一步的按钮）
  * */

  const SUDOKU = {
    // 始初化
    initialize() {
      const t = this
      document.querySelector('.list ul').innerHTML = new Array(81).fill(0).map((v, i) => `<li>
            <input type="text" maxlength="1" pattern="^[1-9]{1}$" data-index="${i}"></li>`).join('')
      const inputs = Array.from(document.querySelectorAll('ul input'))
      const getKey = (index) => Math.trunc(index / 27) * 3 + Math.trunc(index % 9 / 3)
      const block = [] // 9个方块集合
      inputs.forEach((e, index) => {
        const i = getKey(index)
        if (!block[i]) {
          block[i] = []
        }
        block[i].push(index)
      })
      const list = inputs.map((el, index) => {
        const rowIndex = index % 9 // 列序号
        const lineIndex = Math.trunc(index / 9) * 9 // 行序号
        const relationList = [...block[getKey(index)]]
        for (let j = 0; j < 9; j++) {
          relationList.push(lineIndex + j)
          relationList.push(rowIndex + j * 9)
        }
        const relation = new Set(relationList.filter(i => i !== index))
        return {
          index, // 序号
          val: '', // 当前值
          emptyList: [], // 尝试前要清空的列表
          isInit: false, // 初始化值
          relation: [...relation], // 相关格子序号【不包括自己】
          el, // input标签
        }
      })
      Object.assign(t, {
        but: document.querySelector('button[name="result"]'),
        list,
        testList: [], // 测试列表（已排序）
        random: 0.5,// 随机率
        isRand: false,
        result: [],// 成功结果列表
      })
    },
    _getVal(item) {
      let arr = ['1', '2', '3', '4', '5', '6', '7', '8', '9']
      const set2 = new Set(item.relation.map(i => this.list[i].val))
      arr = arr.filter(v => !set2.has(v))
      if (t.isRand) {
        arr.sort(() => Math.random() - 0.5) // 打乱
      }
      return arr
    },
    // 预处理
    pretreatment(lock = false) {
      const obj = {err: false, reTry: true, done: false}
      while (!obj.err && obj.reTry && !obj.done) {
        obj.reTry = false
        const arr = this.list.filter(o => !o.val)
        if (arr.length === 0) {
          obj.done = true
          this.result.push(this.list.map(o => o.val))
        } else {
          arr.forEach((e) => {
            if (!obj.err) {
              const val = this._getVal(e)
              if (val.length === 0) {
                obj.err = true
                // console.log('无解！！！', e.index)
              } else if (val.length === 1) {
                e.val = val[0]
                e.el.value = val[0]
                if (lock) {
                  e.el.className = 'lock'
                }
                obj.reTry = true
              } else {
                e._val = val
              }
            }
          })
        }
      }
      return obj
    },
    sortItem() {
      const arr = this.list.filter(o => !o.val)
      arr.sort((a, b) => a._val.length - b._val.length)
      arr[0].emptyList = arr
      return arr[0]
    },
    begin(index) {
      const t = this
      const item = t.testList[index]
      if (!item) {
        t.but.click()
        return null; // console.log('无解，结束了！', index)
      }
      // 恢复其他空值
      item.emptyList.forEach(o => {
        o.val = ''
      })
      const val = item._val.pop()
      if (!val) {
        t.testList.pop(); // 删除失败元素
        index-- // 失败！ 后退尝试
      } else {
        item.val = val
        const {done, err} = t.pretreatment()
        if (done) {
          // console.log('成功了！！！')
          if (t.isRand) {
            t.isRand = false
            t.result[0].forEach((v, n) => {
              const val = Math.random() < t.random ? '' : v
              const item = t.list[n]
              item.val = item.el.value = val
              item.el.disabled = !!val
            })
            return null
          }
        } else if (!err) {
          t.testList.push(t.sortItem())
          index++
        }
      }
      return t.begin(index) // 尾调用优化
    },
  }

  window.onload = function () {
    let resultIndex = 0
    window.t = SUDOKU
    t.initialize()
    document.addEventListener('focusout', e => {
      const tag = e.target
      if (tag.tagName === 'INPUT' && tag.type==='text') {
        const val = tag.value
        const item = t.list[tag.dataset.index]
        if (/^\d$/.test(val) && t._getVal(item).includes(val)) {
          item.val = val
          t.pretreatment()
        } else {
          tag.value = ''
        }
        //生成提示
        t.list.forEach(o => {
          console.log('xxxxx',o.el.parentNode)
          if (o.val) {
            o.el.parentNode.removeAttribute('data-tip')
          } else {
            o.el.parentNode.setAttribute('data-tip', t._getVal(o).join(''))
          }
        })
      }
    }, false)
    document.querySelector('input[type="range"]').addEventListener('change', e => {
      t.random = Number(e.target.value)
      e.target.nextElementSibling.innerText = {
        0: '小白',
        0.1: '菜鸟',
        0.2: '初级',
        0.3: '中级',
        0.4: '高级',
        0.5: '专家',
        0.6: '魔鬼',
        0.7: '无敌',
      }[t.random]
    })
    document.addEventListener('click', e => {
      const tag = e.target
      if (tag.tagName === 'BUTTON') {
        switch (tag.name) {
          case 'rand':
            t.result = [] //清空结果
            t.isRand = true
            const arr = ['1', '2', '3', '4', '5', '6', '7', '8', '9']
            arr.sort(() => Math.random() - 0.5) // 打乱
            const list = {}
            while (arr.length) {
              const index = Math.random() * 81 | 0
              list[index] = arr.pop()
            }
            t.list.forEach((e, index) => {
              e.val = e.el.value = list[index] || ''
              e.el.disabled = false
              e.el.className = ''
            })
            t.pretreatment()
            t.testList = [t.sortItem()]
            t.begin(0)
            t.but.innerText = '结果'
            break
          case 'begin':
            t.list.forEach((e) => {
              e.val = e.el.value || ''
              e.isInit = !!e.val
              e.el.disabled = !!e.val
            })
            const obj = t.pretreatment(true)
            t.result = [] //清空结果
            t.but.dataset.index = '0'
            if (obj.err) {
              t.but.innerText = '无解'
            } else if (obj.done) {
              t.but.innerText = '已解'
              t.list.forEach(o => o.el.value = o.val)
            } else {
              t.but.innerText = '结果'
              t.testList = [t.sortItem()]
              try {
                t.begin(0)
              } catch (e) {
                t.but.click()
                alert('结果太多了，有点烧脑无法继续！')
              }
            }
            break
          case 'result':
            const len = t.result.length
            if (len) {
              if (!e.isTrusted || resultIndex >= len) {
                resultIndex = 0
              }
              t.result[resultIndex].forEach((v, n) => {
                t.list[n].el.value = v
              })
              resultIndex++
              tag.innerText = `结果 ${resultIndex}/${len}`
            } else {
              tag.innerText = '无解'
            }
            break
          default:
        }
      }
    }, false)
  }
</script>
</body>
</html>
